////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2003-2006 Adobe Macromedia Software LLC and its licensors.
//  All Rights Reserved. The following is Source Code and is subject to all
//  restrictions on such code as contained in the End User License Agreement
//  accompanying this product.
//
////////////////////////////////////////////////////////////////////////////////

package mx.charts.chartClasses
{

import flash.display.Graphics;
import flash.geom.Rectangle;
import mx.charts.HitData;
import mx.charts.series.items.HLOCSeriesItem;
import mx.charts.series.renderData.HLOCSeriesRenderData;
import mx.collections.CursorBookmark;
import mx.core.IDataRenderer;
import mx.core.IFactory;
import mx.core.IFlexDisplayObject;
import mx.core.mx_internal;
import mx.graphics.IStroke;
import mx.graphics.LinearGradientStroke;
import mx.graphics.Stroke;
import mx.styles.ISimpleStyleClient;

use namespace mx_internal;

//--------------------------------------
//  Styles
//--------------------------------------

include "../styles/metadata/ItemRendererStyles.as"

/**
 *  HLOCSeriesBase is the base class for the two financial series types,
 *  HLOCSeries and CandlestickSeries.
 *  Most of the behavior related to charting open, close, high and low values
 *	resides in this class.
 */
public class HLOCSeriesBase extends Series implements IColumn
{
    include "../../core/Version.as";

	//--------------------------------------------------------------------------
	//
	//  Constructor
	//
	//--------------------------------------------------------------------------
	
	/**
	 *  Constructor.
	 */
	public function HLOCSeriesBase()
	{
		super();

		_instanceCache = new InstanceCache(null, this);
		_instanceCache.properties = { styleName: this };
	}

	//--------------------------------------------------------------------------
	//
	//  Variables
	//
	//--------------------------------------------------------------------------
	
	/**
	 *  @private
	 */
	private var _instanceCache:InstanceCache;
	
	/**
	 *  @private
	 */
	mx_internal var _renderData:HLOCSeriesRenderData;
	
	//--------------------------------------------------------------------------
	//
	//  Properties
	//
	//--------------------------------------------------------------------------
	
    //----------------------------------
	//  closeField
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the closeField property.
	 */
	private var _closeField:String = "";
	
    [Inspectable(category="General")]

	/**
	 *  Specifies the field of the data provider that determines
	 *  the y-axis location of the closing value of the element.
	 *
	 *  @default ""
	 */
	public function get closeField():String
	{
		return _closeField;
	}
	
	/**
	 *  @private
	 */
	public function set closeField(value:String):void
	{
		_closeField = value;

		dataChanged();
	}

    //----------------------------------
	//  columnWidthRatio
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the columnWidthRatio property.
	 */
	private var _columnWidthRatio:Number = 0.65;
	
    [Inspectable(category="General", defaultValue="0.65")]

	/**
	 *  Specifies the width of elements relative to the category width.
	 *  A value of <code>1</code> uses the entire space, while a value
	 *  of <code>0.6</code> uses 60% of the element's available space. 
	 *  You typically do not set this property directly. 
	 *  The actual element width used is the smaller of the
	 *  <code>columnWidthRatio</code> and <code>maxColumnWidth</code>
	 *  properties.
	 *
	 *  @default 0.65.
	 */
	public function get columnWidthRatio():Number
	{
		return _columnWidthRatio;
	}		
	
	/**
	 *  @private
	 */
	public function set columnWidthRatio(value:Number):void
	{
		_columnWidthRatio = value;
		
		invalidateTransform();
	}
	
    //----------------------------------
	//  highField
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the highField property.
	 */
	private var _highField:String = "";
	
    [Inspectable(category="General")]

	/**
	 *  Specifies the field of the data provider that determines
	 *  the y-axis location of the high value of the element. 
	 *
	 *  @default ""
	 */
	public function get highField():String
	{
		return _highField;
	}
	
	/**
	 *  @private
	 */
	public function set highField(value:String):void
	{
		_highField = value;

		dataChanged();
	}

    //----------------------------------
	//  items
    //----------------------------------

	/**
	 *  @private
	 */
	mx_internal function get items():Array
	{
		return _renderData ? _renderData.filteredCache : null;
	}

    //----------------------------------
	//  itemType
    //----------------------------------

	/**
	 *  The subtype of ChartItem used by this series
	 *  to represent individual items.
	 *  Subclasses can override and return a more specialized class
	 *  if they need to store additional information in the items.
	 */
	protected function get itemType():Class
	{
		return HLOCSeriesItem;
	}
	
    //----------------------------------
	//  lowField
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the lowField property.
	 */
	private var _lowField:String = "";
	
    [Inspectable(category="General")]

	/**
	 *  Specifies the field of the data provider that determines
	 *  the y-axis location of the low value of the element.
	 *
	 *  @default ""
	 */
	public function get lowField():String
	{
		return _lowField;
	}
	
	/**
	 *  @private
	 */
	public function set lowField(value:String):void
	{
		_lowField = value;

		dataChanged();
	}

    //----------------------------------
	//  maxColumnWidth
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the maxColumnWidth property.
	 */
	private var _maxColumnWidth:Number;
	
    [Inspectable(category="General")]

	/**
	 *  Specifies the width of the elements, in pixels.
	 *  The actual element width used is the smaller of this style
	 *  and the <code>columnWidthRatio</code> property.
	 *  You typically do not set this value directly;
	 *  it is assigned by the enclosing chart. 
	 */
	public function get maxColumnWidth():Number
	{
		return _maxColumnWidth;
	}
	
	/**
	 *  @private
	 */
	public function set maxColumnWidth(value:Number):void
	{
		_maxColumnWidth = value;
		
		invalidateTransform();
	}

    //----------------------------------
	//  offset
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the offset property.
	 */
	private var _offset:Number = 0;

    [Inspectable(category="General", defaultValue="0")]

	/**
	 *  Specifies how far to offset the center of the elements
	 *  from the center of the available space,
	 *  relative to the category width. 
	 *  At the value of default <code>0</code>,
	 *  the elements are centered on the space.
	 *  Set to <code>-50</code> to center the element
	 *  at the beginning of the available space.
	 *  You typically do not set this property directly.
	 *  The enclosing chart control manages this value based on 
	 *  the value of its <code>columnWidthRatio</code> property.
	 */
	public function get offset():Number
	{
		return _offset;
	}
	
	/**
	 *  @private
	 */
	public function set offset(value:Number):void
	{
		_offset = value;
		
		invalidateTransform();
	}

    //----------------------------------
	//  openField
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the openField property.
	 */
	mx_internal var _openField:String = "";
	
    [Inspectable(category="General")]

	/**
	 *  Specifies the field of the data provider that determines
	 *  the y-axis location of the opening value of the element.
	 *
	 *  @default ""
	 */
	public function get openField():String
	{
		return _openField;
	}
	
	/**
	 *  @private
	 */
	public function set openField(value:String):void
	{
		_openField = value;

		dataChanged();
	}

    //----------------------------------
	//  renderDataType
    //----------------------------------

	/**
	 *  The subtype of ChartRenderData used by this series
	 *  to store all data necessary to render.
	 *  Subclasses can override and return a more specialized class
	 *  if they need to store additional information for rendering.
	 */
	protected function get renderDataType():Class
	{
		return HLOCSeriesRenderData;
	}

    //----------------------------------
	//  xField
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the xField property.
	 */
	private var _xField:String = "";

    [Inspectable(category="General")]

	/**
	 *  Specifies the field of the data provider that determines
	 *  the x-axis location of the element. 
	 *  If set to the empty string (<code>""</code>),
	 *  Flex renders the columns in the order they appear in the dataProvider.
	 *
	 *  @default ""
	 */
	public function get xField():String
	{
		return _xField;
	}
	
	/**
	 *  @private
	 */
	public function set xField(value:String):void
	{
		_xField = value;

		dataChanged();
	}

	//--------------------------------------------------------------------------
	//
	//  Overridden methods: Series
	//
	//--------------------------------------------------------------------------
				
	/**
	 *  @inheritDoc
	 */
	override protected function updateData():void
	{
		var renderDataType:Class = this.renderDataType;
		_renderData = new renderDataType();

		_renderData.cache = [];
		var i:int = 0;
		var itemClass:Class = itemType;

		if (cursor != null)
		{
			cursor.seek(CursorBookmark.FIRST);
			while (!cursor.afterLast)
			{
				_renderData.cache[i] = new itemClass(this, cursor.current, i);
				i++;
				cursor.moveNext();
			}
		}

		cacheIndexValues(_xField, _renderData.cache, "xValue");

		cacheDefaultValues(_highField, _renderData.cache, "highValue");

		cacheNamedValues(_lowField, _renderData.cache, 
		"lowValue");
		if (_openField != "")
			cacheNamedValues(_openField, _renderData.cache, "openValue");
		
		cacheNamedValues(_closeField, _renderData.cache, "closeValue");

		super.updateData();
	}

	/**
	 *  @inheritDoc
	 */
	override protected function updateMapping():void
	{
		dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).
			mapCache(_renderData.cache, "xValue", "xNumber", _xField == "");
		
		var yAxis:IAxis = dataTransform.getAxis(
							CartesianTransform.VERTICAL_AXIS);
		
		yAxis.mapCache(_renderData.cache, "lowValue", "lowNumber");

		yAxis.mapCache(_renderData.cache, "highValue", "highNumber");
		
		if (_openField != "")
			yAxis.mapCache(_renderData.cache, "openValue", "openNumber");
		
		yAxis.mapCache(_renderData.cache, "closeValue", "closeNumber");

		super.updateMapping();		
	}

	/**
	 *  @inheritDoc
	 */
	override protected function updateFilter():void
	{
		if (filterData)
		{
			_renderData.filteredCache = _renderData.cache.concat();

			dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).
				filterCache(_renderData.filteredCache, "xNumber", "xFilter");
			
			var yAxis:IAxis = dataTransform.getAxis(
								CartesianTransform.VERTICAL_AXIS);
			
			yAxis.filterCache(_renderData.filteredCache,
							  "lowNumber", "lowFilter");
			
			yAxis.filterCache(_renderData.filteredCache,
							  "highNumber", "highFilter");
			
			if (_openField != "")
			{
				yAxis.filterCache(_renderData.filteredCache,
								  "openNumber", "openFilter");
			}
			
			yAxis.filterCache(_renderData.filteredCache,
							  "closeNumber", "closeFilter");

			stripNaNs(_renderData.filteredCache, "xFilter");

			stripNaNs(_renderData.filteredCache, "lowFilter");

			stripNaNs(_renderData.filteredCache, "highFilter");
			
			if (_openField != "")
				stripNaNs(_renderData.filteredCache, "openFilter");
			
			stripNaNs(_renderData.filteredCache, "closeFilter");
		}
		else
		{
			_renderData.filteredCache = _renderData.cache;
		}

		super.updateFilter();
	}
	
	/**
	 *  @inheritDoc
	 */
	override protected function updateTransform():void
	{
		dataTransform.transformCache(_renderData.filteredCache,
									 "xNumber", "x", "highNumber", "high");
		
		dataTransform.transformCache(_renderData.filteredCache,
									 null, null, "highNumber", "high");
		
		dataTransform.transformCache(_renderData.filteredCache,
									 null, null, "lowNumber", "low");
		
		if (_openField != "")
		{
			dataTransform.transformCache(_renderData.filteredCache,
										 null, null, "openNumber", "open");
		}
		
		dataTransform.transformCache(_renderData.filteredCache,
									 null, null, "closeNumber", "close");

		var unitSize:Number = dataTransform.getAxis(
								CartesianTransform.HORIZONTAL_AXIS).unitSize
		
		var params:Array =
		[
			{ xNumber: 0 },
			{ xNumber: _columnWidthRatio * unitSize / 2 },
			{ xNumber: _offset * unitSize }
		];

		dataTransform.transformCache(params, "xNumber", "x", null, null);

		_renderData.renderedHalfWidth = params[1].x -  params[0].x;
		
		if (_offset == 0)
			_renderData.renderedXOffset = 0;
		else
			_renderData.renderedXOffset = params[2].x -  params[0].x;
		
		if (!isNaN(_maxColumnWidth) &&
			_maxColumnWidth < _renderData.renderedHalfWidth)
		{
			_renderData.renderedXOffset *= _maxColumnWidth /
										   _renderData.renderedHalfWidth;

			_renderData.renderedHalfWidth = _maxColumnWidth;
		}

		super.updateTransform();
	}

	/**
	 *  @inheritDoc
	 */
	override protected function updateDisplayList(unscaledWidth:Number,
												  unscaledHeight:Number):void
	{
		super.updateDisplayList(unscaledWidth, unscaledHeight);

		if (!dataTransform)
			return;
			
		var g:Graphics = graphics;
		g.clear();

		if (!isNaN(_maxColumnWidth) &&
			(_maxColumnWidth <= 0 || _columnWidthRatio <= 0))
		{
			return;
		}

		var renderData:HLOCSeriesRenderData =
			transitionRenderData == null ?
			_renderData :
			HLOCSeriesRenderData(transitionRenderData);

		var renderCache:Array = renderData.filteredCache;

		var i:int;
		var sampleCount:int = renderCache.length; 
		var rc:Rectangle;
		var instances:Array;
		var inst:IFlexDisplayObject;
		var v:HLOCSeriesItem;
		
		_instanceCache.factory = getStyle("itemRenderer");
		_instanceCache.count = sampleCount;			
		instances = _instanceCache.instances;
		
		var bSetData:Boolean = sampleCount > 0 &&
							   (instances[0] is IDataRenderer);

		if (transitionRenderData != null &&
			transitionRenderData.elementBounds != null)
		{
			var elementBounds:Array = transitionRenderData.elementBounds;

			for (i = 0; i < sampleCount; i++)
			{
				inst = instances[i];
				
				v = renderCache[i];
				v.itemRenderer = inst;
				if (bSetData)
					(inst as IDataRenderer).data = v;
				
				rc = elementBounds[i];
				inst.move(rc.left, rc.top);
				inst.setActualSize(rc.width, rc.height);
			}
		}
		else
		{
			var ro:Number = renderData.renderedHalfWidth +
							renderData.renderedXOffset;

			var lo:Number = -renderData.renderedHalfWidth +
							renderData.renderedXOffset;

			rc = new Rectangle();
			var considerOpen:Boolean = _openField != "";
			
			for (i = 0; i < sampleCount; i++)
			{
				v = renderCache[i];

				rc.left = v.x + lo;
				rc.right = v.x + ro;
				rc.top = Math.min(v.high,Math.min(v.low,v.close));
				if (considerOpen)
					rc.top = Math.min(rc.top,v.open);
				rc.bottom = Math.max(v.high,Math.max(v.low,v.close));
				if (considerOpen)
					rc.bottom = Math.max(rc.bottom,v.close);

				inst = instances[i];
				v.itemRenderer = inst;
				if (bSetData)
					(inst as IDataRenderer).data = v
				inst.move(rc.left,rc.top);
				inst.setActualSize(rc.width,rc.height);
			}
		}
	}

	/**
	 *  @inheritDoc
	 */
	override public function describeData(dimension:String,
										  requiredFields:uint):Array
	{
		validateData();

		if (_renderData.cache.length == 0)
			return [];

		var description:DataDescription = new DataDescription();
		description.boundedValues = null;

		if (dimension == CartesianTransform.VERTICAL_AXIS)
		{
			extractMinMax(_renderData.cache, "highNumber", description);
			extractMinMax(_renderData.cache, "lowNumber", description);
		}
		else if (dimension == CartesianTransform.HORIZONTAL_AXIS)
		{
			if (_xField != "")
			{
				if ((requiredFields &
					 DataDescription.REQUIRED_MIN_INTERVAL) != 0)
				{
					// If we need to know the min interval,
					// then we rely on the cache being in order.
					// So we need to sort it if it hasn't already been sorted.
					var sortedCache:Array = _renderData.cache.concat();
					sortedCache.sortOn("xNumber", Array.NUMERIC);		
					extractMinMax(sortedCache, "xNumber", description,
								  (requiredFields &
								   DataDescription.REQUIRED_MIN_INTERVAL) != 0);
				}
				else
				{
					extractMinMax(_renderData.cache, "xNumber", description,
								  (requiredFields &
								   DataDescription.REQUIRED_MIN_INTERVAL) != 0);
				}
			}
			else 
			{
				description.min = _renderData.cache[0].xNumber;
				description.max =
					_renderData.cache[_renderData.cache.length - 1].xNumber;
				if ((requiredFields &
					 DataDescription.REQUIRED_MIN_INTERVAL) != 0)
				{
					extractMinInterval(_renderData.cache, "xNumber",
									   description);
				}
			}

			description.padding = 0.5;
		}
		else
		{
			return [];
		}
			
		return [ description ];
	}

	/** 
	 *  Generates a text description of a ChartItem
	 *  suitable for display as a DataTip.
	 */
	protected function formatDataTip(hd:HitData):String
	{
		var dt:String = "";
		var n:String = displayName;
		if (n != null && n.length>0)
		dt += "<B>" + n + "</B><BR/>";
		
		var xName:String = dataTransform.getAxis(
			CartesianTransform.HORIZONTAL_AXIS).displayName;
		
		if (xName != "")
			dt += "<I>" + xName + ":</I> ";
		
		dt += dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).
			  formatForScreen(HLOCSeriesItem(hd.chartItem).xValue) + "\n";

		var yName:String = dataTransform.getAxis(
			CartesianTransform.VERTICAL_AXIS).displayName;

		if (_openField != "")
		{
			if (yName != "")
				dt += "<I>" + yName + " (open):</I> ";
			else
				dt += "<I>open:</I> ";
			
			dt += dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).
				  formatForScreen(HLOCSeriesItem(hd.chartItem).openValue) + "\n";
		}
		
		if (yName != "")
			dt += "<I>" + yName + " (close):</I> ";
		else
			dt += "<I>close:</I> ";
		
		dt += dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).
			  formatForScreen(HLOCSeriesItem(hd.chartItem).closeValue) + "\n";

		if (yName != "")
			dt += "<I>" + yName + " (high):</I> ";
		else
			dt += "<I>high:</I> ";
		
		dt += dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).
			  formatForScreen(HLOCSeriesItem(hd.chartItem).highValue) + "\n";

		if (yName != "")
			dt += "<I>" + yName + " (low):</I> ";
		else
			dt += "<I>low:</I> ";
		
		dt += dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).
			  formatForScreen(HLOCSeriesItem(hd.chartItem).lowValue) + "\n";
		
		return dt;
	}
	
	/**
	 *  @inheritDoc
	 */
	override public function get legendData():Array
	{
		var ld:LegendData = new LegendData();
		var marker:IFlexDisplayObject;
		
		ld.element = this;
		
		var markerClass:IFactory = getStyle("legendMarkerRenderer");
		if (markerClass == null)
			markerClass = getStyle("itemRenderer");
		
		if (markerClass) 
		{
			marker = markerClass.newInstance();
			if (marker as ISimpleStyleClient)
				(marker as ISimpleStyleClient).styleName = this;
		}

		ld.marker = marker;
		ld.label = displayName;

		return [ ld ];
	}

	/**
	 *  @inheritDoc
	 */
	override public function styleChanged(styleProp:String):void
	{
		super.styleChanged(styleProp);
		
		var styles:String = "stroke fill";
		if (styleProp == null ||
			styleProp == "" ||
			styles.indexOf(styleProp) != -1)
		{
			invalidateDisplayList();
			legendDataChanged();
		}
	}

	/**
	 *  @inheritDoc
	 */
	override protected function get renderData():Object
	{
		if (_renderData == null)
		{
			var renderDataType:Class = this.renderDataType;
			var rv:HLOCSeriesRenderData = new renderDataType();
			
			rv.cache = rv.filteredCache = [];
			rv.renderedHalfWidth = 0;
			rv.renderedXOffset = 0;
			
			return rv;
		}

		return _renderData;
	}
	
	/**
	 *  @inheritDoc
	 */
	override public function beginInterpolation(sourceRenderData:Object,
												destRenderData:Object):Object
	{
		var interpFields:Object =
			{ x: true, high: true, low: true, close: true };
		
		if (_openField != "")
			interpFields.open = true;
		
		var idata:Object = initializeInterpolationData(
			sourceRenderData.cache, destRenderData.cache,
			interpFields, itemType,
			{ sourceRenderData: sourceRenderData,
			  destRenderData: destRenderData });

		var interpolationRenderData:HLOCSeriesRenderData =
			HLOCSeriesRenderData(destRenderData.clone());

		interpolationRenderData.cache = idata.cache;	
		interpolationRenderData.filteredCache = idata.cache;	

		transitionRenderData = interpolationRenderData;
		
		return idata;
	}
	
	
	/**
	 *  @inheritDoc
	 */
	override protected function getMissingInterpolationValues(
									sourceProps:Object, srcCache:Array,
									destProps:Object, destCache:Array,
									index:Number, customData:Object):void
	{
		for (var propName:String in sourceProps)
		{
			var src:Number = sourceProps[propName];
			var dst:Number = destProps[propName];

			if (propName == "high" || propName == "low" ||
				propName == "open" || propName == "close")
			{
				if (isNaN(src))
					src = unscaledHeight;
				if (isNaN(dst))
					dst = unscaledHeight;
			}
			else if (propName == "x")
			{
				if (isNaN(src))
					src = dst;
				if (isNaN(dst))
					dst = src;
			}

			sourceProps[propName] = src;
			destProps[propName] = dst;
		}		
	}

	/**
	 *  @inheritDoc
	 */
	override public function getElementBounds(renderData:Object):void
	{
		var cache :Array = renderData.filteredCache;
		var rb :Array = [];
		var sampleCount:int = cache.length;		

		if (sampleCount)
		{
			var ro:Number = renderData.renderedHalfWidth +
							renderData.renderedXOffset;

			var lo:Number = -renderData.renderedHalfWidth +
							renderData.renderedXOffset;
	
			var v:HLOCSeriesItem = cache[0];
			var maxBounds:Rectangle = new Rectangle(v.x, v.low, 0, 0);
			
			for (var i:int = 0; i < sampleCount; i++)
			{
				v = cache[i];
				
				var top:Number = Math.min(v.high, v.low);
				
				var b:Rectangle = new Rectangle(
					v.x + lo, top, ro - lo, Math.max(v.high, v.low) - top);
				
				maxBounds.left = Math.min(maxBounds.left, b.left);
				maxBounds.top = Math.min(maxBounds.top, b.top);
				maxBounds.right = Math.max(maxBounds.right, b.right);
				maxBounds.bottom = Math.max(maxBounds.bottom, b.bottom);
				
				rb[i] = b;
			}
		}
		else
		{
			maxBounds = new Rectangle();			
		}
		
		renderData.elementBounds = rb;
		renderData.bounds =  maxBounds;
	}
}

}
